I seguenti appunti sono stati presi nell’anno accademico 2022-2023 durante il corso di Architetture dei sistemi di elaborazione.
Il materiale non è ufficiale e non è revisionato da alcun docente, motivo per cui non mi assumo responsabilità per eventuali errori o imprecisioni.
Per qualsiasi suggerimento o correzione non esitate a contattarmi.
E’ possibile riutilizzare il materiale con le seguenti limitazioni:
E’ per tanto possibile:
Questi appunti sono disponbili su GitHub al seguente link:
https://github.com/Guray00/polito_lectures/tree/main/Tecnologie%20e%20Servizi%20di%20Rete
Introduzione al corso
L’affidabilità è spesso misurata utilizzando:
Le tre misure sono legate dalla seguente formula: \[MTTF = MTBF + MTTR\]
Per riuscire a garantire un rateo di “zero guasti” si studia la “bathtub curve”, ovvero la curva che descrive il numero di guasti in funzione del tempo. La curva è caratterizzata da tre fasi:
La performance di un dispositivo può essere analizzata da due punti di vista:
Il tempo che deve essere considerato per la performance sono:
La valutazione della performance viene spesso effettuata eseguendo le applicazioni e valutando il loro comportamento. La scelta dell’applicativo inficia particolarmente sull’analisi, ma nel caso ideale si dovrebbe utilizzare un carico di lavoro paragonabile all’utilizzo utente. Per questo motivo si utilizzano i benchmark, ovvero del software su misura che simulano il comportamento di un utente.
I benchmark spesso vengono utilizzati eseguendo algoritmi (es quicksort molto grosso), programmi reali (compilatore C) o applicazioni apposite.
In particolare noi utilizzeremo MIBench che consente di eseguire test inerenti a vari tipi di applicazioni.
E’ importante garantire la riproducibilità dei test, per questo motivo è importante utilizzare uno stesso hardware e software per tutti i test (oltre al programma di input).
Può essere interessante avere una media pesata dei risultati, in modo da poter valutare la performance in base al tipo di applicazione.
Le linee guida per la misurazione della performance si basano su due principi:
La legge di Amdahl è una formula che descrive il miglioramento della performance in funzione del numero di processori. La formula è la seguente:
\[\text{speedup} = \frac{\text{performance with enhancement}}{\text{performance with without enhancement}}\]
Lo speedup risultante da un miglioramento dipende da due fattori:
\[ \text{execution time new} = \text{execution time old} * ((1 - \text{fraction enhanced}) + \frac{\text{fraction enhanced}}{\text{speedup enhanced}})\]
\[\text{speedup overall} = \frac{\text{execution time old}}{\text{execution time new}} = \frac{1}{(1 - \text{fraction enhanced}) + \frac{\text{fraction enhanced}}{\text{speedup enhanced}}}\]
Supponiamo di avere una macchina che è 10 volte più veloce nel 40% dei programmi che girano. Quale è lo speedpup totale?
\[\text{fraction enhanced} = 0.4\] \[\text{speedup enhanced} = 10\] \[\text{speedup overall} = \frac{1}{(1 - 0.4) + \frac{0.4}{10}} = 1.56\]
Sono disponibili due soluzioni per migliorare la performane di una macchina floating point:
Quale soluzione rende più rapida la macchina? Per rispondere è sufficiente riapplicare la legge di Amdahl.
\[\text{speedup1} = \frac{1}{(1-0.2)+ \frac{0.2}{10}} = 1.22\]
\[\text{speedup2} = \frac{1}{(1-0.5)+ \frac{0.5}{2}} = 1.33\]
La soluzione 2 è più vantaggiosa.
Per misurare il tempo richiesto per eseguire un programma sono utilizzabili 3 approcci:
La terza opzioni consiste nel calcolo della seguente formula:
\[ \text{CPU time} = (\sum_{i=1}^{n} CPI{i} * IC{i}) * \text{clock cycle time} \]
L’instruction set Architecture (ISA) è come il computer è visto da un programmatore e dal compilatore. Ci sono molte alternative per un designer ISA, che possono essere valutati in base a vari criteri:
Le CPU sono spesso classificate in accordo al loro tipo di memoria interna:
Attualmente tutti i processori utilizzano General Purpose Register senza accedere direttamente alla memoria. Non hanno dunque dei ruoli specifici, anche se arm in alcuni casi fa eccezione. Questo è un favore perché risultano più veloci rispetto alla lettura in memoria ed è più semplice per il compilatore per gestire le variabili.
Esistono due tipi di memorizzazione in accordo all’andianess:
Dunque se leggiamo un dato nel modo sbagliato avremo i dati invertiti.
I dati possono essere salvati in modo:
La memoria può essere acceduta in tre differenti modi:
ADD R4, R3ADD R4, #3ADD R4, 100(R1)ADD R4, (R1)ADD R3, (R1+R2)ADD R1, (1001)ADD R1, @(R3)ADD R1, (R2)+ADD R1, -(R2)ADD R1, 100(R2)[R3]Scegliere una metodologià piuttosto che un’altra può portare a ridurre il numero di istruzioni o aumentare la complessità dell’architettura CPU o aumentare l’average CPI. Quello più diffuso è sicuramente con displacement. La dimensione dell’indirizzo in modalità displacement dovrebbe essere tra 12 e 16 bit mentre la dimensione per la immediate field dovrebbe essere tra 8 e 16 bit.
Le istruzioni di controllo possono essere divise in quattro categorie:
Gli indirizzi di destinazione sono normalmente specificati come displacement rispetto al valore corrente del program counter. In questo modo:
Le chiamate a procedure e i salti indiretti mediante registri consentono di scrivere codice che include salti che non sono conosciuti a tempo di compilazione e di implementare case o switch statements. Supporta le librerie condivise dinamicamente e le funzioni virtuali (chiamare differenti funzioni in base al tipo di dato)
Nel caso di utilizzo di procedure, alcune informazioni devono essere salvate:
Riassumendo: Poche istruzioni sono realmente indispensabili come load, store, add subtract, move register-register, and, shift compare equal, compare not equal, branch, jump, call e return. Branch displacements relativo al program counter dovrebbe essere di almeno 8 bit, mentre register-indirect e PC-relative addressing possono essere utilizzati nelle chiamate alle procedure e ritorno.
Gli operandi supportati sono:
L’encoding dell’instruction set dipende dalle istruzioni che compongono il set e dai metodi di indirizzamento supportati. Quando un gran numero di metodi di indirizzamento sono supportati, un indirizzo specificato in un campo è utilizzato per specificare l’addressing modo e il registro che potrebbe essere coinvolto. Quando il numero di è invece basso, possono essere encodati insieme all’opcode.
Il designer dove far attenzione a problemi di conflitto dovuti alla dimensione del codice o alla dimensione dell’instruction set, il numero di metodi di indirizzamento e il numero di registro, oltre alla complessità di fetch e decoding hardware.
Ormai sono poche le persone che sviluppano direttamente in assembly, in quanto ormai i programmi odierni fanno utilizzo di compilatori largamente ottimizzati. Si pongono allora alcuni problemi relativi alla allocazione delle variabili all’interno dei registri, fase cruciale in fase di ottimizzazione da parte di un compilatore. E’ possibile ottimizzare il tempo di accesso alle variabili allocandole all’interno dei registri solo se queste sono salvate nello stack o sono variabili globali in memoria. Non è pertanto possibile per le variabili nello heap, a causa di problemi di allineamento.
Le raccomandazioni sono di avere almeno 16 registri ed essere semplici e ortogonali.
Il MIPS prende il nome da Microprocessor without Interlocked Pipeline Stages, e fa parte della famiglia di processori RISC. Il primo processore è stato inventato nel 1985 a cui si sono susseguiti ulteriori versioni. Quello che si è scoperto è che dimunendo la complessità di ogni passaggio si rendeva più veloce il funzionamento, dunque rimuovendo il sistema di interlock.
Questo tipo di processori quando eseguono un operazione in memoria si limitano a fare “solamente questo”. Hanno un simple load-store instruction set. Sono pensati per l’efficienza delle pipeline, in particolare con una lunghezza di istruzioni prefissata e pensate per applicazioni a basso consumo energetico (a differenza dei processori SISC). Il misc per tanto potrebbe risultare più compatto in quanto ogni istruzione fa più operazioni, ma a costo di una maggiore complessità.
I registri sono a 64 bit e il registro 0 è sempre 0 (non
R0). Questo consente di utilizzare metodi di indirizzamento
alternativi rispetto a quelli già visti.
I tipi di dato utilizzabili sono i classici:
Viene utilizzato 16 bit di immediate field. Il primo registro è quello destinazione, mentre il secondo e terzo campo sono gli operandi.
LD R1, 30(R2) // carica il valore di R2 + 30 in R1
R2 = XX
R1 <- MEM[30 + R2]
Una istruzione CPU è un single 32 bit aligned word. Include un opcode di 6 bit iniziali. Le istruzioni sono in 3 formati:
Il primo tipo è quello immediato, caratterizzato da:
Il secondo tipo è quello registro, caratterizzato da:
Il terzo tipo è quello salto, caratterizzato da:
le istruzioni sono raggruppato per il loro funzionamento:
Nota: le istruzioni sono lunghe 32 bit.
I processori MIPS utilizzano un architettura di caricamento e salvataggio, attraverso le quali avviene l’accesso alla memoria principale.
LB R1, 28(R8)LD R1, 28(R8)LBU R1, 28(R8)L.S F4, 46(R5).L.D F4, 46(R5)SD R1, 28(R8)SW R1, 28(R8)SH R1, 28(R8)SB R1, 28(R8)Ovviamente avviene l’estensione dei valori ripetendo il bit più significativo. Nel floating point il primo bit è il segno. Attenzione: per L.S abbiamo il risultato nella parte più significativa.
Tutte le operazioni vengono eseguiti con operandi memorizzati nei registri. Le istruzioni possono essere di tipo immediato, con due operandi, shift, moltiplicazione, divisione, ecc. oltre ad aritmetica in complemento a due come somma, sottrazione, moltiplicazione, divisione.
DADDU R1, R2, R3DADDUI R1, R2, 74LUI R1, 0X47J nameJAL nameJALR R4La pipeline è un implementazione che consente di eseguire più istruzioni in modo sovrapposto durante l’esecuzione. In questo modo, differenti unità (chiamate pipe stages o segmenti) sono eseguite in parallelo ed eseguono parti differenti.
Il throughput rappresenta il numero di istruzioni che vengono processate per unità di tempo. Tutte gli stage sono sincronizzate e il tempo per eseguire il primo ste è chiamato machine cycle, e normalmente corrisponde a un ciclo di clock. La lunghezza del machine cycle è determinato dallo stage più lento. Siamo in grado di eseguire CPI (clock Cycles Per Instruction) clock cycles per istruzione.
In una pipeline ideale, tutti gli stage sarebbero perfettamente bilanciati e sarebbe dovuto a:
\[\text{throughput}_{pipelined} = \text{throughput}_{unpipelined} * n\]
con n pari al numero di stages.
Prendiamo come esempio una implementazione senza pipeline. L’esecuzione di ogni istruzione potrebbe essere composta di al più 5 clock cycles:
Tutte le istruzioni richiedono dunque 5 clock cycle, tranne le istruzioni branch a cui ne bastano 4. Ottimizzazioni potrebbero essere fatte per ridurre il CPI medio: come esempio, le istruzioni alu potrebbero essere completate durante il cycle di MEM. Le risorse hardware potrebbero essere ottimizzato per eliminare duplicazioni. Si potrebbe prendere in considerazione un’architettura single clock alternativa. E’ necessario l’utilizzo di un single control unit per produrre i segnali necessari al datapath.
Un esempio di una versione pipelined prevede l’avvio di una nuova istruzione per ogni clock cycle. Inoltre, differenti risorse lavorano mediante differenti istruzioni contemporaneamente. Per ciascun clock cycle, ciascuna risorsa può essere utilizzata per solo una richiesta; ciò significa che è necessario separare le istruzioni e la memoria dati e che il register file è utilizzato nello stadio di lettura in ID e per scrittura in WB. Deve dunque essere disegnato per soddisfare queste necessità nello stesso clock cycle. Il program counter deve essere cambiato nello stadio di IF (facendo attenzione nei casi dei salti). Infine, si vede necessario introdurre i pipeline register, ovvero dei registri intermedi.
Nota: si da per scontato che i dati necessari siano già stati caricati in cache.
La pipeline aumenta il throughput del processore senza dover rendere più veloce le singole istruzioni. Le istruzioni processato sono fatte rallentate da pipeline control overheads. La profondità della pipeline è limitata dalla necessità di bilanciare gli stati e dal overhead.
Gli hazards sono situazioni che possono far si che un istruzione non venga eseguita come dovrebbe. Ci sono tre tipi di hazard:
A causa dei pipeline hazards sono necessari gli stalli, dove si richiede ad alcuni processi di fermarsi per non causare problemi, per uno o più cicli di clock. Questo fa in modo che le istruzioni che verranno dopo una certa istruzione non vengano eseguite, mentre quelle indietro continuano ad essere eseguite. Gli stalli causano dunque l’introduzione di una sorta di “bolla” all’interno della pipeline.
I structural hazards possono avvenire quando una unità della pipeline non è in grado si eseguire una certa operazione che era stata pianificata per quel cycle. Alcuni esempi potrebbero essere:
La soluzione è inevitabilmente il miglioramento dell’hardware o l’acquisto di nuove componenti.
Gli hazard di dati sono quelli relativi alle dipendenze dei dati che vengono elaborati alterando, ad esempio, l’ordine di lettura e scrittura degli operandi e causando risultati sbagliati o non deterministici.
Se avviene un interruzione durante l’esecuzione di una porzione di codice critica la correttezza potrebbe essere ripristinata, ma causando quello che potrebbe essere un comportamento non deterministico.
Per risolvere questi problemi si potrebbe utilizzare:
Un hardware dedicato all’interno del datapath si occupa di rilevare quando un precedente operazione alu dovrebbe essere scritta nel registro che corrisponde al sorgente della operazione ALU. In questo caso, l’hardware selezione il risultato come input piuttosto che il valore nel registro. Deve, inoltre, essere in grado di fare il forwarding dei dati da ogni istruzione iniziata in precedenza e allo stesso modo di non fare forwarding se l’istruzione che segue è in stallo oppure è stata eseguita una interruzione.
Si hanno dunque data hazard quando vi è dipendenza tra le istruzioni e sono abbastanza vicine da essere sovrapposte a causa della pipeline. Questo generalmente avviene per operandi che sono sia registri che memoria, in particolare se la memoria subisce load e store non nello stesso stage o se l’esecuzione procede mentre un istruzione è in attesa di risoluzione di una cache miss.
Un esempio può essere il seguente:
; istruzioni 1 2 3 4 5 6 7 8
LD R1, 0(R2) F D E M W -> 5
SUB R4, R1, R5 F D s E M W -> 2
ADD R6, R1, Ry F s D E M W -> 1
Il controllo avviene nella fase di decodifica, e richiede di individuare un possibile data hazard relativa a una istruzione in fase di ID. Se venisse rilevata, due strade sono possibili:
Quando una istruzione di load va in fase di esecuzione e un’altra istruzione sta cercando di accedere al dato carico in fase di decode, dovranno essere eseguiti dei controlli per verificare se gli operandi fanno match, e nel caso rilevare il data hazard e come risultato l’unità di controllo deve inserire nella pipeline uno stallo per prevenire le istruzioni di fetch e decode di avanzare.
| opcode di ID/EX | opcode di IF/ID | match? |
|---|---|---|
| Load | register-register alu | ID/EX.IR[rt] == IF/ID.IR[rs] |
| Load | register-register alu | ID/EX.IR[rt] == IF/ID.IR[rt] |
| Load | load, store, ALU immediate, branch | ID/EX.IR[rt] == IF/ID.IR[rs] |
Data una istruzione attuamente in stage di decodifica, introdurre uno stallo in fase di esecuzione è possibile:
Invece, l’introduzione di un forwarding può essere implementato:
Inoltre, deve necessariamente eseguire le comparazioni tra il destination fiel del IR contenuto nel EX/MEM e MEM/WB registers con il source fiel del IR contenuto nel IF/IDm ID/EX, EX/MEM registers.
Sono dovuti a salti (condizionali o meno) che possono cambiare il program couter dopo che l’istruzione ha già eseguito il fetch. Nel caso in cui siano condizionali, la decisione di come varia il program counter dipende da quale branch verrà eseguito. Nella implementazione MIPS, il PC è scritto con il target address (se è preso) alla fine della fase di decode.
Una possibile soluzione si basa sull’utilizzo di stalli appena l’istruzione di branch viene individuata (in fase di decode) e decidere anticipatamente se il salto avverrà o meno e calcolare in anticipo il nuovo valore del program counter.
Un esempio senza ottimizzazione:
; istruzioni 1 2 3 4 5 6 7 8
nez R1, cont F D E M W
1 F D
2 F
:
1 F D E M W
2
Con l’ottimizzazione:
; istruzioni 1 2 3 4 5 6 7 8
nez R1, cont F D E M W
1 F
2
:
1 F D E M W
2
Il costo è l’aumento di hardware e fare attenzione ai registri che sono legate alle istruzioni di salto in modo che siano corretti.
Per evitare problemi può essere necessario introdurre stalli per avere consistenti i valori nei registri, riprendendo l’esempio di prima:
; istruzioni 1 2 3 4 5 6 7 8
addi R1, R1, 1 F D E M W
nez R1, cont F s D E M W
1 s F
2
:
1 F D E M W
2
Ci sono varie tecniche per ridurre la degradazione delle performance dovute ai salti:
La prima alternativa, freezing the pipeline, è quella già proposta che prevede che la pipeline sia posta in stallo (o svuotata) appena l’istruzione di branch è rilevata e fino a quando non si conosce dove saltare. E’ la soluzione più semplice da implementare.
La predict untaken assume che il branch non venga preso e evita qualsiasi cambio di stato della pipeline fino a quando il branch non ha compuuto la decisione. Inoltre, annulla le operazioni eseguite se invece il branch viene preso. Lo stesso approccio può essere utilizzato nel predict taken se si è a conoscenza del il target address prima del risultato del branch. In questi casi il compilatore utilizza delle ottimizzazioni interne per aumentare le probabilità da parte del processore di fare la giusta previsione (ad esempio realizzando strutture for più semplici da valutare).
Il delayed branch si basa sull’idea di riempire gli slot dopo l’istruzione di branch, denominati branch.delay slot, con istruzioni che devono essere eseguite indipendentemente dall’esito del branch. E’ compito del compilatore eseguire aggiungere le istruzioni corrette e non esegue nulla in particolare quando l’istruzione di branch è decodificata. A volte però non è semplice trovare le istruzioni di delayed slot, per cui negli ultimi anni è meno adottata.
Fino ad ora abbiamo lavorato con istruzioni che richiedono un colpo di clock per essere eseguite. In questo capitolo vedremo come gestire istruzioni che richiedono più cicli di clock per essere eseguite.
Le operazioni sui numeri floating point è più complicato rispetto agli interi. Per questo motivo, per riuscire a svolgere tali operazioni ini un solo ciclo di clock il designer sarebbe costretto:
Nessuna di queste due è però realmente fattibile, per cui si cerca di scomporre l’operazione in più fasi che vengono eseguite in una sorta di pipeline. Otteniamo quindi una versione che paralelizza le diverse attività. Tutte le unità convergono nella fase di mem per poi concludere nella fase di write back. E’ solo la fase di execute che risulta parallelizzata.
Si vede necessario definire:
Questo crea però come problema quello di una comparsa più frequente degli hazard.
A causa della dell’impossibilità di utilizzare una pipeline per l’unità di divisione, molte istruzioni potrebbero necessitarne allo stesso tempo. Inoltre, è possibile ottenere risultati dalle diverse unità operazionali nello stesso tempo (non è realmente possibile).
Il simulatore funziona in modo leggermente diverso, per cui alcuni casi potrebbero comportarsi diversamente.
La soluzione è quello di utilizzare ulteriori write ports (però molto costosa) oppure forzare uno structural hazard:
A causa di un più unga latenza nelle operazioni, gli stalli per i data hazards possono fermare una pipeline per un quantitativo di tempo maggiore.
Inoltre, nuovi tipi di data hazards sono possibili a causa dei tempi maggiori per raggiungere la write back.
La soluzione è quello di controllare, prima di entrare in fase di esecuzione, se l’istruzione andrà a scrivere su un registro che è già in fase di esecuzione.
Non sempre ci si ferma in fase di decodifica, può succedere che si fermi anche in fase di execute.
Si utilizza la S per gli stalli strutturali ed
s per gli stalli dovuti a data hazards.
Il MIPS R4000 è un processore a 64 bit introdotto il 1991, con istruzioni simili al MIPS64. Utilizzava una pipeline a 8 stage per risolvere i problemi di cache dovuti agli accessi e una frequenza di clock più elevata. L’accesso alla memoria erano stati dunque divisi in più passi. Le pipeline più lunghe prendono spesso il nome di superpipelines.
Alcune caratteristiche sono:
L’unità floating poin è composta di 3 unità funzionali: divider, multiplier, adder. Era composta da 8 stage differenti:
Esistono due approcci per l’Instruction level parallelism:
Più frequenti soprattuto nel mercato desktop e server, ma anche inccluso in prodotti come:
L’approccio statico è meno frequente, ma prevalente in quello che è il mercato embedded.
Un basic block è una sequenza di istruzioni senza alcun branch-in, ad esclusione dell’ingresso, e senza branch-out, ad esclusione dell’uscita.
Il compilatore potrebbe rischedulare le istruzioni per ottimizzare il codice, ad esempio:
a = b + c
d = e - f
Assumendo che la load abbia una latenza di 1 clock cycle, un codice che implementa questo funzionamento sono:
LD Rb, b
LD Rc, c
ADD Ra, Rb, Rc
SD Ra, Va
LD Re, e
LD Rf, f
SUB Rd, Re, Rf
SD Rd, Vd
LD Rb, b IF ID EX MEM WB
LD Rc, c IF ID EX MEM WB
ADD Ra, Rb, Rc IF ID st EX MEM WB
SD Ra, Va IF st ID EX MEM WB
LD Re, e IF ID EX MEM WB
LD Rf, f IF ID EX MEM WB
SUB Rd, Re, Rf IF ID st EX MEM WB
SD Rd, Vd IF st ID EX MEM WB
Sono necessari 14 clock. Si potrebbe cambiare l’ordine delle operazioni ottenendo:
LD Rb, b IF ID EX MEM WB
LD Rc, c IF ID EX MEM WB
LD Re, e IF ID EX MEM WB
ADD Ra, Rb, Rc IF ID EX MEM WB
LD Rf, f IF ID EX MEM WB
SD Ra, Va IF ID EX MEM WB
SUB Rd, Re, Rf IF ID EX MEM WB
SD Rd, Vd IF ID EX MEM WB
In questo modo ottimizzato sono invece necessari solo 12 cicli di clock.
Per il MIPS le i basic block sono solitamente lunghi tra le 4 e le 7 istruzioni. Dal momento che le istruzioni potrebbero dipendere da altre, il parallelismo dei basic block è limitato, per tale motivo si utilizzano ulteriori tecnic di parallelizzazione come ad esempio quelle sui cicli.
Prendendo come esempio il seguente codice:
for (i=0; i < 1000; i++)
x[i] = x[i] + y[i];Ogni iterazione del ciclo è indipendente dalle altre, quindi è possibile eseguire le istruzioni in parallelo. Esistono due modalità per fare ciò:
Il loop unrolling è una tecnica che consiste nel duplicare il codice all’interno del ciclo, in modo da eseguire più istruzioni in parallelo.
// originale
for (i=0;i<N;i++ ) {
body
}// unrolled
for (i=0;i<N/4;i++ ){
body
body
body
body
}Nell’esempio di prima otterremo:
or (i=0;i<N;i=i+4 ){
x[i] = x[i]+ y[i];
x[i+1] = x[i+1]+ y[i+1];
x[i+2] = x[i+2]+ y[i+2];
x[i+3] = x[i+3]+ y[i+3];
}I vantaggi sono che riusciamo a ridurre il numero di controlli che vengono effettuate e aumentiamo le chance che il compilatore elimini gli stalli. Lo svantaggio è l’aumento della dimensione del codice.
Le single instruction stream possono essere portate a multiple data streams (SIMD) adoperando vector processors, istruzioni vettoriali che lavorano su un set di dati (rispettoa d dati scalari), l’utilizzo di Graphics Processing Units e l’utilizzo di differenti unità funzionali per eseguire task simili in parallelo lavorando su multipli dati.
Abbiamo 3 tipi di dipendenze di dato:
Si dice che c’è una dipendenza di dati se una istruzione i dipende da una istruzione j con i che deve produrre un risultato che viene utilizzato da j oppure se l’istruzione j dipende da una istruzione k che è a sua volta dipendente da i.
Un esempio è mostrato di seguito:
Loop: L.D F0, 0(R1)
ADD.D F4, F0, F2
S.D F4, 0(R1)
La seconda istruzione utilizza come sorgente il prodotto della prima, mentre la terza ugualmente riscrive nel medesimo registro causando delle dipendenze tra i dati.
Le dipendenze sono proprietà del programma, mentre gli hazards sono proprietà della pipeline. Gli stalli dipendono dal programma e dalla pipeline.
Rilevare le dipendenze che riguardano i registri è semplice, ma se sono prese in considerazione celle di memoria potrebbe essere molto più complicato in quanto l’accesso alla medesima cella potrebbe essere molto complicato. Se viene utilizzata una strategia statica, il compile deve adottare un approccio conservativo assumendo che ogni istruzione di load faccia riferimento alla stessa cella o a una precedentemente salvata. Questo tipo di dipendenze possono essere rilevate solo a runtime.
Le dipendenze di nome avvengono quando due istruzioni fanno riferimento allo stesso registro o alla stessa locazione di memoria (nome) ma non c’è un flusso di dati associato al nome. Ci sono due tipi di dipendenze di nome tra una istruzione i e un istruzione j:
Loop: L.D F0, 0(R1)
ADD.D F4, F0, F2
S.D F4, 0(R1)
L.D F0, -8(R1)
ADD.D F4, F0, F2
S.D F4, -8(R1)
L.D F0, -16(R1)
...
tra la riga 2 e la 4 c’è antidependence, mentre tra la 1 e la 4 c’è output dependence.
Strategia secondo cui si cerca di identificare i casi in cui la dipendenza dei dati possa non esistere utilizzando un ulteriore registro.
DIV.D F0, F2, F4
ADD.D F6, F0, F8
S.D F6, 0(R1)
SUB.D F8, F10, F14
MUL.D F6, F10, F8
Potrebbe essere risolto nel seguente modo:
DIV.D F0, F2, F4
ADD.D F6, F0, F8
S.D F6, 0(R1)
SUB.D T, F10, F14
MUL.D F6, F10, T
e inoltre:
DIV.D F0, F2, F4
ADD.D S, F0, F8
S.D F6, 0(R1)
SUB.D F8, F10, F14
MUL.D S, F10, F8
Read after write hazards, corrispondono a una dipendenza di dati reale.
Sono possibili se le istruzioni scrivono in uno o più stae o
…
Dipendenze che avvengono quando un istruzione dipende da un branch. Ad esempio:
if p1 {
S1;
}
j
if p2 {
S2;
}S1 è dipendente da p1, ed S2 è dipendnente da p2.
Bisogna fare attenzione nel caso in cui avvengano modifiche che alterano il flusso di dati ed è fondamentale evitarlo.
Le eccezzioni sono eventi interni che modificano la normale esecuzione del programma. Nel caso invece di eccezzioni dovute a effetti esterni si parla di interruzioni.
Le cause di eccezzioni sono varie:
Le eccezioni si classificano in:
La maggior parte dei dispositivi sono catalogati come restartable machines, ovvero dato una eccezione è in grado di ripartire da dove si era bloccata.
Quando avviene una eccezione, la pipeline deve eseguire i seguenti step:
Una volta terminata la gesitone, una istruzione speciale fa tornare la macchina all’origine dell’eccezione e ricarica il PC originale facendo ripartire lo stream di istruzioni.
Un processore può gestire le eccezioni in modo preciso e in modo non preciso. Quando avviene una istruzione che rilascia eccezione tutte le precedenti devono essere completate, mentre quelle che seguono vengono rieseguite dall’inizio. Ripartire dopo una eccezione potrebbe essere molto complicato se non gestite in modo preciso, per questo è necessario nella maggior parte delle architetture (perlomeno per le istruzioni intere). Questo ha però un costo in termina di performance.
Garantire eccezioni precise è più complicato con le multiple cycle instructions. Un esempio è mostrato di seguito:
DIV.D F0, F2, F4
ADD.D F10, F10, F8
SUB.D F12, F12, F14
L’istruzione ADD.D e SUB.D sono completate prima di DIV.D (out-of-order completion). Se dovesse essere rilasciata una eccezione da parte di SUB.D, questa sarebbe gestita in modo impreciso.
La soluzione implementabili sono vari:
| Pipeline stage | Cause of exception |
|---|---|
| IF | Page fault on instruction fetch, Misaligned memory access, Memory-protection violation |
| ID | Undefined or illegal opcode |
| EX | Arithmetic exception |
| MEM | Page fault on data fetch, Misaligned memory access, Memory-protection violation |
| WB | None |
Immaginiamo di avere una eccezione nella MEM di LD e nella EX di DADD:
LD IF ID EX MEM WB
DADD IF ID EX MEM WB
Potrebbe avvenire una data page fault exception nella fase di MEM per LD e una eccezione di tipo aritmetico nello stage EX per DADD. La price eccezione viene processata e, se la causa dovesse essere rimossa, la seconda eccezione viene gestita.
Esistono però dei casi in cui le due eccezioni possono avvenire in ordine inverso a quelli a cui fanno riferimento:
LD IF ID EX MEM WB
DADD IF ID EX MEM WB
La soluzione potrebbe essere:
Quando una istruzione è garantito che finisca è detta committed. Alcune macchine hanno istruzioni che possono cambiare stato prima di essere committed. Se una di queste istruzioni venisse abortita a causa di una eccezione, lascerebbe la macchina in uno stato alterando e rendendo nuovamente di difficile implementazione le eccezioni precise.
Instructions implicitly updating condition codes possono creare complicazioni:
Le istruzioni complesse sono difficili da implementare nella pipeline, forzando ad avere la stessa lunghezza. Per questo motivo a volte i problemi sono risolti inserendo nella pipeline microistruzioni che implementano ciascuna istruzione.
Le prestazioni delle memorie cache sono molto più veloci rispetto alle memorie, sia hard disk che ram.
Le cache funzionano secondo due principi:
Dunque se un blocco intero viene caricato in memoria cache a t0, è molto probabile che a un certo tempo \[\delta t\] il programma troverà in cache tutte le word necessarie.
E’ necessario definire alcuni elementi:
Il tempo di accesso alla memoria medio sarà:
\[t_{access} = h * C + (1 - h) * M\]
Normalmente i valori per h sono nell’ordine di 0.9.
L’equazione non è però accurata in quanto prende in considerazione solo
un tipo di cache.
La cache si divide in una parte di data e una parte di controllo. La parte di data a sua volta e divisa in un directory array un data array., in cui ogni entry è una cache line caratterizzatà da un bit di validità, un tag e un blocco di dati.
Ogni cache line può contenere un blocco di memoria con a sua volta più word. A ciascuna è associato un tag field che indica il blocco di memoria presente in quel momento. Inoltre, la cache contiene la logica ricevere gli indirizzi prodotti dal processore, controllare al suo interno per vedere se è presente e nel caso caricare il blocco.
Un data block può avere una dimensione differente e in numero differente all’interno di una cache line.
Il tag è una parte dell’indirizzo dove la linea di cache si trova in memoria, non è necessario salvare 32 bit dell’indirizzo.
I bit di validità invece indicano se il blocco è presente o meno. Il numero di bit di validità può avere più bit pari al numero di data block presenti.
Si parla di cache hit quando la richiesta di data, che viene ricevuta dalla cache, è presente all’interno della cache. Si parla di cache miss quando la richiesta di data non è presente all’interno della cache.
Ogni blocco di memoria e data block è pari a un byte, per cui se ho una memoria di 1024 byte avrò 1024 cache line, in quanto ogni cache ha un data block. Quando un dato della memoria deve essere indirizzato è composto da:
Per individuare un blocco in cache è sufficiente un multiplexer (decoder) che opera sull’indirizzo:
Nel caso di 1024 byte saranno sufficienti 10 bit per l’index, 3 bit per l’offset e 19 bit per il tag.
Dunque il diagramma totale appare come segue:
La cache è normalmente situata ta il CPU e il bus (oppure tra la memoria principale e il bus, ma non conviene). Ogni volta che il processore effettua un accesso in memoria allora la cache interpreta l’indirizzo e controlla se la word è già in cache o meno.
Nel caso di un cache hit, la cache riduce il tempo di accesso alla memoria di un fattore dipendente dal ratio tra il tempo di accesso alla cache e alla memoria.
Nel caso di una miss, la cache può rispondere in due modi:
Le cache oggi giorno sono separate in cache di istruzioni e cache di dati. La cache per le istruzioni è solitamente pià semplice da gestire rispetto a quella dei dati, in quanto le istruzioni non possono essere cambiate.
Se sono utilizzate due cache, l’architettura del sistema ricade nello schema denominato “Harvard architecture”, caratterizzato dall’esistenza di due memorie separate tra dati e codice (in contrasto con quella di Von Neumann).
Le caratteristiche sono:
La dimensione della cache è molto importante in termini di costi e performance. Man mano che la dimensione aumenta si ha un incremento dei costi, delle prestazioni di sistema ma una riduzione della velocità della cache. Le dimensioni solitamente variano da qualche kB a qualche MB.
Il meccanismo attraverso cui una linea viene associata ad un blocco di memoria è detto mapping. E’ importante assicurarsi che la verifica di presenza di un dato per un certo indirizzo sia sufficientemente veloce.
Il tipo di mappatura è detto modello di assocatività, e può essere:
Ciascun blocco di memoria è associato staticamento a un set
k in cache utilizzando l’espressione:
\[ k = index \ mod \ n \]
dove n è il numero di linee nella cache. Il calcolo di
k può essere fatto semplicemente prendendo i bit meno
significativi dell’index.
Il vantagigo è che può essere implementato semplicemente in hardware, lo svantaggio è che se il programma accede frequentemente a due blocchi corrispondenti alla stessa cache line, avviene una miss a ciascun accesso in memoria.
Per calcolare dove scrivere il valore è necessario trovare il numero di set:
\[s = N / W\]
Dove N è il numero di cache lines e W il numero di word lines.
Un blocco è associato a un set k mediante:
\[ k= i \ mod \ s\]
Il blocco i può essere inserito in una qualsiasi delle W
linee del set k. Una cache set associative con W linee in ogni set è
detta cache a W-linee. Solitamente W a un valore di 2 o 4.
Ciascun blocco della memoria principale può essere messo in un blocco qualsiasi della cache. Il vantaggio è una maggiore flessibilità nello scegliere un blocco, ma a costo di una maggiore complessità hardware nella ricerca.
Per sostituire le chace line deve essere utilizzato un algoritmo che individui quale rimuovere. Le scelte possibili sono:
Esiste anche il pLRU che è un approssimazinoe efficiente di LRU. L’età di ciascuna via della cache è mantenuta in un albero binario, di cui ogni nodo rappresenta una “history bit”. Quando avviene un accesso, viene fatto il toggle dei bit corrispondenti incontrati.
Nel caso FIFO viene utilizzato l’algoritmo second
chance: ogni elemento ha un bit di utilizzo. Quando viene
utilizzato un nodo viene posto a 1 il bit, dandogli una
seconda “chance”, perchè essendo appena stato letto potrebbe essere
ancora utile. In modo sequenziale vengono controllati tutti i nodi, fino
a quando non viene trovato uno di valore 0, che viene
rimosso. Se un nodo viene trovato con il bit a 1 viene
posto a 0 e si continua a cercare. Se tutti i nodi sono
stati utilizzati ha un comportamento FIFO.
Quando avviene una operazione di scrittura su un dato presente in cache, è necessario aggiornare anche il dato presente in memoria principale. Per fare ciò esistono due soluzioni:
Per ogni cache block, un flag denominato dirty bit, indica se il blocco è stato cambiato o meno da quando è stato caricato in cache. La scrittura in memoria principale avviene solo quando il blocco viene sostituito dalla cache ed è settato il dirty bit.
gli svantaggi di questo approccio sono:
Ogni volta che la CPU effettua una operazione di scrittura, questo viene scritto sia in cache che in memoria principale. La conseguente perdita di efficienza è limitata dal fatto che le operazioni di scrittura sono solitamente molto meno numerose di quelle di lettura.
La coerenza tra le cache è uno dei problemi priincipali tra i sistemi multiprocessore con memoria condivisa, in cui ogni processore ha una propria cache. Lo stesso tipo di problema si verifica se è presente un DMA controller.
Per risolvere questo problema viene introdotto il validity bit per ogni cache line. Se è disabilitato, allora non è stato effettuato nessun accesso al blocco e deve dare una miss. All’avvio tutti i validity bit sono disabilitati.
Può essere conveniente utilizzare più bit di cache avere più livelli di cache:
Un esempio è AMD Sambezi, facente parte della AMD fusion family. Include 8 core con ciascuno una cache di livello 1. Ciascuna coppia di processori ha un secondo livello da 2 o 4 Mbytes e infine i core dello stesso device condividono un terzo livello di cache da 8 mbytes.
Immaginiamo di avere una memoria cache con le seguenti caratteristiche:
Determina la struttura della cache (numero di line, dimensione del tag field).
Ogni blocco è di 4 byte, quindi se ho \(2^32\) byte avrò 2^30 blocchi. Se la cache ha 64 kbyte, avrò 2^16 blocchi quindi la cache ha 2^14 linee. Il tag è composto da 30 bit, ma 14 fanno riferimento alla linea e dunque solo 16 sono per il tag field.
La dimensione totale della cache è dunque:
\[2^14 * (32+16) = 2^14 *48 = 768 kbit = 96kByte\]
I salti possono potenzialmente impattare in modo molto rilevante sulle prestazioni delle pipeline.. Per questo motivo è possibile ridurre la perdita di performance andando a eseguire delle predizioni su quello che sarà il risultato del salto.
Lo scopo è quello di eseguire correttamente il forecasting branches, riducendo le chance che il controllo dipenda da cause di stallo. Possiamo categorizzare gli schemi in due gruppi:
L’accuratezza della predizione non aumenta in modo significativo da un aumento della dimensione del buffer o del numero dei bit utilizzati per la predizione.
Può essere utile in combinazioni con altre tecniche statiche come l’ééenabling delayed branches** e il rescheduling to avoid data hazards.
Il compilatore può prevedere il comportamento in modo differenti:
Nei programmi per SPEC92 il 34% delle predizioni basate su branch sempre presi erano errate, con un rate molto variabile da 9% al 59%. Altre tecniche potrebbero avere un comportamento migliore nel caso medio, ma sono ancora con alte variazioni da programma a programma.
Nel caso di predizioni basate su branch direction si ha che i forward branches solotimanete sono poco presi mentre i backwar branches sono molto spesso presi (come ad esempio in un ciclo).
Il profiling è una tecnica che può essere utilizzata per prevedere una sequenza tipica di input che riceve il programma ed eseguire il programma un numero limitato di volte utilizzando tale input, eseguendo una statistica in base al comportamento dei branch.
Gli schemi dinamici sono implementati in hardware, e utilizzano gli indirizzi delle instruzioni branch per attivare meccanismi differenti di predizioni. Possono essere implementati seguendo tecniche differenti:
E’ il metodo più semplice di predizione dinamica. La branch History Table (BHT) è una piccola memoria indicizzata dalla più piccola porzione di indirizzo dell’istruzione di branch. Ciascuna entry ha uno o più bit che registrano se il branch è stato preso o meno l’ultima volta che è stato eseguito.
Ogni volta che viene decodificata una istruzione di branch, viene eseguito un accesso alla BTH utilizzando la porzione di indirizzo per l’indice. La predizione viene salvata nella tabella utilizzata, e il nuovo program counter viene calcolato in accordo a tale predizione. Quando il risultato di un branch è noto, la tabella viene aggiornata.
L’efficacia dipende dal metodo utilizzato, ma soffre del problema di aliasing (una linea della tabella potrebbe far riferimento a un altro salto e non quello che sto eseguendo) e dell’acuratezza della predizione.
Nei processori MIPS, la voalutazione della condizione del branch viene eseguita quando viene identificata l’istruzione di branch, per tale motivo non porta nessun reale vantaggio.
Se prendiamo un esempio di un loop che è preso 9 volta di seguito e poi al decimo deve uscire. Se ipotizziamo che la entry non è condivisa con altri branch, la predizione utilizzando una BHT di 1bt, verrà sbagliata solo la prima e l’ultima iterazione con un successo del 80%, minore del fatto che venga assunto che venga sempre preso (90%).
Gli schemi predittivi a 2 bit permettono una capacità di predizione più elevata. Per ogni branch vengono mantenuti due bit, la predizione è cambiata solo dopo due miss.
Attenzione: Non si comporta come un contatore
Gli schemi predittivi a n-bit permettono una capacità di predizione ancora più elevata, e rappresenta un caso più generale di quella precedente. Il counter, che si comporta effettivamente come un contatore, è aumentato di uno ogni volta che il branch viene preso e decrementato in caso contrario. Quando il counter è maggiore di metà del suo valore massimo, il branch viene predetto come preso, altrimenti come non preso.
Alcuni esperimenti hanno mostrato come ci sia un vantaggio con n>2.
Viene inoltre denominata bimodal branch prediction.
Le performance dei branch vengono impattate da alcuni fattori:
IN questo tipo di predittori, la storia dei salti precedenti influenza la scelta attuale di predizione.. Questo approccio, denominato anche two–level predictors, è dunque basato sulla dipendenza tra i risultati degli ultimi branch.
Utilizzano il comportamento degli ultimi m branch per scegliere da 2^m branch predictors, ciascuno ad n-bit.
L’hardware necessario per implementare lo schema è molto semplice:
In questo caso abbiamo m=1 ed n=1. Ciascun
branch è associato con \(2^m\)
predictors di n bits:
Un alto esempio potrebbe essere con m=2 ed
n=2, con la seguente costruzione:
Come si vede è sufficiente uno shift register con due bit.
Peer ridurre il numero di effetti sul controllo delle dipendenze richieste da sapere appena possibile, come l’informazione che il branch sia preso o meno e il numovo valore del program counter (se il branch è assuto di essere prese), vengono risolti attraverso l’introduzione del branch-target buffer (o cache).
Ciascuna entry del branch-target buffer contiene l’indirizzo del branch considerato e il target value da caricare nel program counter.
Utilizzando un branch-target buffer, il program counter è caricato con il numovo valore alla fine dello stage di fetch, prima ancora che la branch instruction sia decodificata.
Questa tecnica è molto buona, ma ha però un costo molto elevato.
Se viene utilizzato un meccanismo con di predizione a 2-bit, è possibile combinare un branch-target buffer con un branch prediction buffer.
La tabella di predizioni mediante saturated counter è acceduta concatenando il branches global history (GR) con il branch address (PC).
La tabella di predizione è ottenuta effettuando lo XOR tra il branches global history (GR) e il branch address (PC).
Incredibilmente funziona meglio del gselect.
Lo scheduling dinamico consente di identificare le dipendenze che sono sconosciute a tempo di compilazione. Semplifica il lavoro del compilatore e permette al processore di tollerare ritardi non predicibili. Inoltre, permette di eseguire lo stesso codice su differenti processori.
DIV.D F0, F2, F4
ADD.D F10, F0, F8
SUB.D F12, F8, F14La pipeline ha uno stallo dopo la DIV.D a causa della
dipendenza tra DIV.D e ADD.D. La
SUB.D si pone in pausa a sua volta, anche se in realtà non
vi è nessuna dipendenza con le istruzioni precedenti. Potremmo riuscire
ad aumentare le prestazioni rimuovendo la necessità di eseguire le
operazioni in ordine.
L’esecuzione fuori ordine prevede l’esecuzione di istruzioni che non sono state ancora eseguite. Questo può però comportare problemi, in particolare intruducendo:
Se l’esecuzione out-of-order è concessa, potrebbe diventare impossibile riuscire a fare una gestione precisa delle eccezioni. Questo potrebbe comportare che quando una eccezione è rilasciata, una istruzione precedente o successiva all’eccezione ha ancora da essere completata. In entrambi i casi potrebbe essere difficoltoso far ripartire il programma.
Per consentire l’esecuzione fuori ordine, è necessario dividere l’ID-Stage in due parti:
La fase di issue legge l’struzione da un registro o da una coda (sceitta nella fase di fetch). Solo dopo si dovrà attendere per gli operandi e solo a quel punto entrare in fase di esecuzione.
Le istruzioni possono essere messe in stato di stallo o bypassare durante le fasi di lettura degli operandi, per questo motivo potrebbe entrare in una fase di esecuzione out of order
Se il processore include più unità funzionali, molte istruzioni possono essere eseguite in parallelo. Anche in questo dcaso le istruzioni potrebbero bypassare durante le fasi di esecuzione, per questo motivo possono uscire dalla esecuzione out-of-order.
Alcune strategie hardware potrebbero essere applicate per risolvere i problemi di scheduling dinamico. In particolare:
Roberto Tomasulo è stato architetto del processore IBM 360/91, le sue idee sono state pubblicate in un paper del 1967. Le stesse idee vennero poi riutilizzate per il primo processore superscalare costruito.
Le principali idee sono:
Le reservation stations sono la novità chiave nell’approccio di Tomasulo, hanno diverse funzioni:
Ciascuna reservation station relativa a un’unità funzionale controlla quando una istruzione può iniziare l’esecuzione in quella unità.
Ogni volta che viene impartita una istruzione, il registro specifica che gli operandi pendenti sono rinominato ai nomi delle reservation station in carica di calcolarli.
Questo implementa una strategia di register renaming, in grado di eliminare gli hazard WAW e WAR.
I risultati sono passati direttamente alle altre unità funzionali, piuttosto che andare nei registri. Tutti i irsultati dalle unità funzionali e dalla memoria sono inviati sul Common Data Bus, il quale:
Le istruzioni vengono eseguite in 3 fasi:
Possono avere una lunghezza differente.
Quando una istruzione viene presa dalla coda (strategia FIFO), se non sono disponibili reservation station allora avviene uno structural hazard e l’istruzione è posta in stallo fino a quando una reservation station non diventa disponibile. Se invece è disponibile una reservation station, vi viene inviata l’istruzione con gli operandi se sono disponibili, altrimenti si attende anche la loro disponibilità.
Quando un operando appare sul CDB, viene letto dal reservation unit e appena tutti gli operandi dell’istruzione sono disponibili nella reservation unit, l’istruzione può essere eseguita. In questo moto sono eliminate le RAW hazards.
L’e istruzioni di load e store eseguite in due step:
Per evitare di modificare il comportamento delle eccezione, nessuna istruzione è autorizzata a inziare l’esecuzione fino a quando tutti i branch precedenti non sono stati completati. Per questo motivo la speculazione potrebbe essere implementata per migliorare questo meccanismo.
Quando il risultato dell’istruzione è disponibile, viene immediatamente scritto nel CDB, dove i registri e le unità funzionali attendono.
In questo step le istruzioni di step scrivono in memoria.
ciascuna reservation station è associata a un identificiatore. Questo identifica anche gli operandi necessari per l’istruzione, è in questo modo che quest’ultima li riconosce.
Gli identifiers hanno anche il compito di implementare funzionare come virtual register che possono essere utilizzati per implementare il register renaming.
Ogni reservation station ha i seguenti campi:
Ogni elemento in un register file contiene un campo Qi. Questo, contiene il numero della reservation station che continene l’istruzione il cui risultato dovrebbe essere salvato in un registro. Se il Qi è null, neon è attualmente attiva nessuna istruzione che sta calcolando un risultato per quel registro.
In questo modo la hazard detection logic è distribuita e gli stalli per WAW e WAR sono eliminati. Si ha però come contro una alta complessità hardware (includendo un buffer associativo per ogni reservation station) e il CDB potrebbe essere un bottleneck.
Il loop unrolling non è necessario in questa architettura, in quanto possono essere eseguite normalmente in parallelo.
La speculazione basata su hardware è una tecnica per ridurre l’effetto delle dipendenze di controllo in un processore che implementa il dynamic scheduling.
Se un processore supporta la branch prediction con dynamic scheduling, le istruzioni di fetch ed issue sono eseguite come se la predizione fosse sempre corretta.
Se un processore supporta questo tipo di speculazione, le esegue sempre.
Le idee sono principalmente tre
In questo modo il processore implementa una deta flow execution, dove le operazioni sono eseguiti appena sono disponiibli gli operandi.
Viene adottata l’architettura di Tomasulo, che viene però estesa per supportare la speculazione.
La fase di execute si separa nuovamente:
A tal fine viene introdotto il ReOrder Buffer (ROB), una struttura dati che ccontine i risultati delle istruzioni che non hanno ancora effettuato il commit. Si preoccupa di fornire ulteriori virtual register e integra lo store buffer presente nell’architettura originale di Tomasulo.
Con la speculazione, i dati potrebbero essere letti dal ROB se le istruzioni non hanno ancora eseguito commit oppure dal register file in caso contrario.
Ogni entry della ROB ha quaddro campi:
L’esecuzione delle istruzioni avviene in quattro fasi:
Una istruzione viene estratta dalla coda delle istruzioni se è disponibile una empty reservation station e un slot libero nel reorder buffer, in caso contrario l’istruzione viene posta in stallo.
Gli operandi delle istruzioni se presenti nel register file o nel reorder buffer, sono inviati alla riservetion station.
Il numero della entry del reorder buffer dell’istruzione viene vineata alla reservetion station per indicare l’istruzione.
L’istruzione è eseguita appena sono disponibili tutti gli operandi, in modo da evitare hazard di tipo RAW. Gli operando sono presi, se possibile, dal CDB appena una istruzione la produce.
La lunghezza di questo step varia in base al tipo di istruzione (ad esempio 2 per le load, 1 per operazioni intere, variabile per FP).
Scirveremo nel command data bus appena il dato è libero e verrà inviato al reorder buffer.
Tutte le reservation station aspetteranno per il risultato prima di leggerlo, e ciasucna entry verrà poi segnata come disponibile.
Il reorder buffer viene ordinato in base all’ordine originale. Appena una istruzione raggiunge la testa del buffer:
In entrambi i casi, l’entry del reorder buffer viene marchiata come libera.
Il reorder buffer è implementato come una circular buffer.
In seguito alle precedenti implementazioni, gli hazard WAW e WAR non possono più avvenire in quanto il dynamic renaming è implementato e l’aggiornamento della memoria avviene in ordine.
I raw hazards sono evitati in quanto avvieune un enforcing del program order mentre i calcoli dell’effectevive address e la load wrt avvengono prima delle istruzioni di store (????).
Inoltre, una load non entra nel secondo step se qualche entry della ROB è occupata da una store che ha una destination field che è uguale della field che sta facendo la load.
Le istruzioni di store scrivono in memoria solamente quando è stato eseguito il commit. Per questo motivo, gi operandi in input sono richiesti al momento della commit piuttosto che nello stage di scrittura dei risultati. Questo significa che il ROB deve avere un ulteriore campo che specifica da dove gli operandi in input dovranno essere presi per ogni istruzione di store.
Le eccezzioni non sono eseguite appena vengono rilasciate, ma piuttosto quando sono aggiunte al reoder buffer.
Quando una istruzione effettua il commit, la possibile eccezione viene eseguita, e le istruzioni successive vengono eliminate dal buffer.
Se l’istruzione viene eliminata dal buffer, l’esecuzione viene ignorata. Per questo motivo la precise exception handingl è supportata completamente.
Quando avviene un evento costoso in termini di tempo (second-level cache miss, TLB miss) nel caso speculativo, alcuni processori aspettano per la sua esecuzione fino a quando l’evento non è più speculativo, invece gli eventi poco costosi sono normalmente eseguite in modo speculativo.
L’evoluzione del processori superscalare ha portato a complessi processori composti da più unità funzionali, e tutti sono accomunati da una logica per rilevare e gestire le dipendenze, schedulare dinamicamente, previsione e speculazione su branch. Ciò ha comportato un aumento notevole della complessità dei processori.
Sono stati studiati anche approcci alternativi tra cui i VLIW.
I VLIW (Very Long Instruction Word) sono processori che hanno istruzioni molto lunghe e che hanno encodate molte operazioni, che vengono caricate in parallelo.L’hardware include tante unità operazionali quanto quante sono richieste in una singola istruzione.
Questi processori sono molto diffusi per le applicazioni embedded.
Ciò ha comportato un software molto più complesso, in quanto è compito del compilatore decidere quali istruzioni impachettare insieme: exploding parallelism, unrolling loops, scheduling code in basic blocks, etc.
Si ha però una semplificazione dell’hardware, in quanto non è necessario effettuare alcun controllo di dipendeze tra le istruzioni e non è dunque necessario avere un’unità che si occupa di valutare quali istruzioni eseguire in parallelo.
Quando una operazione richiede uno stallo, l’intero pacchetto di istruzioni viene posto in pausa in modo da preservare il flusso deciso dal compilatore.
Le performance che possono essere causate da un multiple issue processor sono limitate dalla limitazioni dei programmi ILP, difficolta di costruire l’hardware, limitazioni specifiche di processori superscalari o vliw.
E’ inoltre difficile trovare un numero sufficiente di istruzioni indipendenti da eseguire in parallelo, soprattutto se consideriamo le unità funzionali pipelined che abbiano una latenza minore di 1.
In generale, per evitare gli stalli sarebbe necessario avere un numero di operazioni indipendenti circa pari a:
\[ \text{avarage pipeline depth} * \text{number of functional units} \]
Con l’incremento di unità funzionali segue un aumento della bandwdth del file register e della memoria. Ciò significa un aumento della complessità hardware e una riduzione delle performance. Alcune soluzioni possibili sono:
La dimensione totale del codice è molto maggiore per i processori VLIW a causa di due fattori principali:
Spesso le istruzioni sono compresse in memoria e poi espanse quando caricate nel processore.
Un processore VLIW richiede spesso accesso alla memoria, che può essere un bottleneck in quanto la bandwidth è magigore e gli stalli per i miss in cash possono porre in attesa l’intero processore.
Un ulteriore problema è la compatibilità dei binari, che non può essere garantita in quanto ogni cambio di implementazione richiede una ricompilazione. Questo è uno dei principali svantaggi rispetto ai processori superscalari, che possono creare facilmente binari compatibili con processori precedenti. Object code translation o l’emulazione possono essere le soluzioni a questo problema.
l’architettura EPIC (Explicitly Parallel Instruction Computing) fu introdotta nella fine degli anni 90 in alcuni processori HP e Intel, come Itanium. Lo scopo era quello di ottenere dei VLIW con una maggiore flessibilità, ottenedo successo nell’area dei processori high-end.
Le istruzioni processate in parallelo richiedono tre task principale:
Arm è un architettura RISC che ha preso gran parte del segmento di processori in ambito mobile ed embedded.
L’architettura ARM si divide in:
La toolchain di sviluppo arm è illustrata nella seguente immagine:
L’architettura generica di un processore arm è il seguente:
Il barrel shifter consente di eseguire gli shift in modo automatico del secondo registro di una operazione senza utilizzare una istruzione apposita.
Quando viene eseguita una istruzione da registro a registro:
Rn ed
RmRd{300px}
Quando viene eseguita una istruzione registro-immediato:
Rn, l’altro è
immediatoRdLe istruzioni di trasferimento dati richiedono due cicli di esecuzione nello stage di esecuzione. Nel primo viene calcolato l’indirizzo utilizzando un registro e un immediato, mentre nel secondo avviene un accesso in memoria nel quale viene trascritto il dato.
Prima viene calcolato l’indirizzo target, aggiungendo un immediato (shiftato di due posizioni) al program counter, dopo la pipeline viene svuotata e riempita nuovamente.
In questo caso, un clock aggiuntivo è necessario in quanto è
necessario salvare l’indirizzo di ritorno in r14.
L’architettura del cortex M3 è rappresentata di seguito:
I salti sono un problema in quanto richiedono 3 cicli per essere completati. Nel caso peggiore, il salto indiretto viene preso e avviene sempre che la pipeline viene svuotate e riempita. Inoltre, non è supportatoto il delayed branch mechanism.
Quando leggiamo della memoria perdiamo un ciclo di clock.
Sono messi a disposizione:
L’instruction set thumb (processori con T nell’acronimo)
sono a 16 bit, sono meno potenti e in numero minore.
Thumb2 venne aggiunto da ARM nel 2003 come suoerset di Thumb (garantendo retrocombabilità) e include nuove istruzioni a 16 bit e alcune a 32 bit. E’ più veloce della prima versione ma riesce a mantenere la dimensione del codice molto compatta.
Il AMBA bus system prevede 3 bus:
La memoria è mappata in 4gb (derivanti dai 32 bit di indirizzi) e la bus matrix viene acceduta mediante i bus AHB e PPB.
{width=400px}
Le eccezioni sono le seguenti:
E’ supportata una interruzione non mascherabile INTNMI. Insieme al processore è presente un Nested Vectored Interrupt Controller (NVIC) che supporta fino a 240 interrruzioni esterne.
I sistemi come come arm v7-M hanno bisogno di due clock:
Il clock della CPU (CCLK) e il clock periferico (PCLK) ricevono il clock input da un PLL (Phase Lock Loop), VPB (VLSI Peripheral Bus) Divider o da un clock esterno.
Dal punto di vistaenergetico sono supportate più modalità di sleep:
Il cortex M3 supporta il clock in tutte le modalità, anche da una fonte esterna. E’ inoltre presente un Wake-UP Interrupt Controller (WIC), overo una fonte esterna di wake-up che permette al processore di spegnersi completamente. Effective with State-Retention / Power Gating (SRPG) methodology.
Le istruzioni sono a 32 (o 16) bit e possono essere eseguite in modo condizionale. E’ presente una architettura di load/store, con la caratteristica che le istruzioni di processazione di dati avvengono solo su registri. Il formato utilizzato prevede 3 operandi e combina ALU e shifter. L’accesso alla memoria avviene con istruioni dotate di auto-indexing.
L’instruction set può essere esteso attraverso dei coprocessori.
In particolare il cortex M3 prevede l’utilizzo di 18 registri a 32 bit che supportano come tipi di dato il byte (8 bit), halfword (16 bit) e word (32 bit).
Il program ounter è il registro r15. Quando il
processore è eseguito in ARM state tutte le istruzioni sono lunghe 32
bit e sono allineate a word. Per questo motivo, il PC value è salvato
nei bit [31:2] con i bit [1:0] pari a
0.
A differenza di molti altri processori come 80x86, ARM consente la
scrittura diretta del Program Counter attraverso il registro
R15.
Il link register è il registro r14, che viene utilizzato
per memorizzare l’indirizzo di ritorno quando il branch con operazioni
link vengono eseguite, calcolate a partire dal PC.
Per ritornare da un linked branch è sufficiente eseguire
MOV r15, r14 oppure MOV pc, lr.
Il registro r13 viene uitlizzato come stack pointer e
viene aggiornato automaticamente, in particolare a tempo di boot viene
recuperato dal Interrupt Vector Table oppure si aggiorna quando il
programma esegue una istruzione stack oriented.
Il Program Status Register è suddiviso in 3 registri:
I flag di stato sono:
La maggior parte delle set instruction permette ai branch di essere eseguite in modo condizionale.
Riutilizzando il condition evaluation hardware, ARM incrementa il modo significativo il numero di istruzioni. tutte le istruzioni contengono un campo condizionale che determina se la cpu le eseguirà o meno. Le istruzioni non eseguite richiedono un ciclo, in quanto necessitano di completare il ciclo per consentire il corretto fetching e decoding delle prossime istruzioni.
Questo permette di rimuovere la necessità di molti branch, che causano lo stallo delle pipeline (3 cicli per il refill). Ciò comporta un very dense in-line code e la penalità di non eseguire le istruzioni condizionali è compensata dal fatto che non è necessario eseguire il branch.
| Codice | Significato |
|---|---|
EQ |
Equal |
NE |
Not equal |
CS |
Carry set (identical to HS), il carry vale 1 |
HS |
Unsigned higher or same (identical to CS) |
CC |
Carry clear (identico a LO), il carry vale 0 |
LO |
Unsigned lower (identical to CC) |
MI |
Minus or negative result |
PL |
Positive or zero result |
VS |
Overflow |
VC |
No overflow |
HI |
Unsigned higher |
LS |
Unsigned lower or same |
GE |
Signed greater than or equal |
LT |
Signed less than |
GT |
Signed greater than |
LE |
Signed less than or equal |
AL |
Always (this is the default |
To execute an instruction conditionally, simply postfix it with the appropriate condition: •For example an add instruction takes the form: • ADD r0,r1,r2 ; r0 = r1 + r2 (ADDAL) •To execute this only if the zero flag is set: • ADDEQ r0,r1,r2 ; If zero flag set then… ; … r0 = r1 + r2 •By default, data processing operations do not affect the condition flags (apart from the comparisons where this is the only effect).
To cause the condition flags to be updated, the S bit of the instruction needs to be set by postfixing the instruction (and any condition code) with an S. ADDS r0,r1,r2 ; r0 = r1 + r2 ; … and set flags
Molto importante!
Moltiplicazione con risultato su 32 bit:
MUL <Rd>, <Rn>, <Rm>Moltiplicazione con risultato unsigned su 64 bit:
UMULL <Rd1>, <Rd2>, <Rn>, <Rm>Moltiplicazione con risultato signed su 64 bit:
SMULL <Rd1>, <Rd2>, <Rn>, <Rm>Nota: Non ci sono differenze tra signed ed unsigned su 32 bit
Attenzione: tutti gli operandi devono essere registri.
MLA <Rd>, <Rn>, <Rm>, <Ra> Rd =
Rn * Rm + RaMLS <Rd>, <Rn>, <Rm>, <Ra> Rd =
Rn * Rm – RaUMLAL <Rd1>, <Rd2>, <Rn>, <Rm>
Rd1,Rd2 = Rn * Rm + Rd1,Rd2SMLAL <Rd1>, <Rd2>, <Rn>, <Rm>
uguale a UMLAL, ma con valori con segnoUDIV <Rd>, <Rn>, <Rm>SDIV <Rd>, <Rn>, <Rm>LSL <Rd>, <Rn>, <op2>Lo shift avviene come ci aspettiamo, inserendo nel bit più significativo degli zeri e spostando il meno significativo nel carry.
LSR <Rd>, <Rn>, <op2>ASR <Rd>, <Rn>, <op2>Le istruzioni di rotazione operano nel seguente modo:
ROR <Rd>, <Rn>, <op2>RRX <Rd>, <Rn>Lo stack ha una organizzazione di tipo Last In - Fist out, ovvero LIFO. I dati sono sempre inseriti (scritti) ed estratti (letti) dal top della pila. Lo stack pointer contiene l’indirizzo del top dello stack.
Lo stack pointer viene aggiornato dopo ogni push e può essere di due tipi:
Mentre il contenuto del top dello stack può essere anch’esso di due tipi:
Nota: il nostro stack è full descending.
Le istruzioni di LDM e STM sono usate per caricare e salvare più registri contemporaneamente.
LDM{xx}/STM{xx} <Rn>{!}, <regList>Dove i parametri sono i seguenti:
Rn è il base registerxx specifica il metodo di indirizzamento, come e quando
rn viene aggiornato durante l’istruzione
!: Rn viene settato al nuovo
valore!: Rn viene impostato al valore
inizialeregList: lista di registriUna lista di registri possono essere:
,Esempi:
{r0-r4, r10, LR}Indicano r0, r1 r2 ,
r3, r4, r10,
r14.
Importante: SP può non apparire nella
lista mentre PC può essere presente solo con
LDM e solamente in assenza di LR.
L’ordine con cui vengono scritti i registri non è importante, questi sono automaticamente ordinati in ordine crescente:
Esempio: {r8, r1, r3-r5, r14} indica
r1, r3, r4, r5, r8, r14.
I metodi di indirizzamento possibili sono due:
IA: increment after (default)
DB: decrement before
Le istruzioni di PUSH e POP facilitano l’utilizzo di un full descending stack:
PUSH <regList> ha il medesimo significato di
STMDB SP!, <regList>POP <regList> ha il medesimo significato di
LDMIA SP!, <regList>Una subroutine viene chiamata con BL <label> e
BLX <Rn>, in particolare viene scritto l’indirizzo
della prossima istruzione in LR e il valore di label o Rn in
PC.
Una procedura reentrant finisce con un branch
all’indirizzo salvato in LR.
E’ possibile, facoltativamente, inziare e terminare una subroutine
con le direttive PRC/FUNCTION e
ENDP/ENDFUNC.
Questo funzionamento genera però problemi nel caso in cui siano
presenti delle chiamate annidate, in quanto il valore in LR
viene sovrascritte non rendendo possibile per sub1
tornarne al main.
Invece di cambiare il valore di LR quando avvengono
delle chiamate annidate, la nuova chiamata potrebbe cambiare il valore
utilizzato nel registro della procedura precedente.
Tutte le subroutine dovrebbero salvare LR e gli altri
registri utilizzati come prima istruzione e ripristinarli come ultima
istruzione:
PUSH {regList, LR}
// ...
POP {regList, PC}E’ possibile passare alle funzioni dei parametri, in particolare sono possibili tre approcci:
MOV r0, #0x34
MOV r1, #0xA3
LDR r3, =mySpace
STMIA r3, {r0, r1}
BL sub2
LDR r2, [r3]
; r2 contains the result
sub2 PROC
PUSH {r2, r4, r5, LR}
LDMIA r3, {r4, r5}
CMP r4, r5
SUBHS r2, r4, r5
SUBLO r2, r5, r4
STR r2, [r3]
POP {r2, r4, r5, PC}
ENDPMOV r0, #0x34
MOV r1, #0xA3
PUSH {r0, r1, r2}
BL sub3
POP {r0, r1, r2}
; r2 contains the result
...
stop B stop
...
sub3 PROC
PUSH {r6, r4, r5, LR}
LDR r4, [sp, #16]
LDR r5, [sp, #20]
CMP r4, r5
SUBHS r6, r4, r5
SUBLO r6, r5, r4
STR r6, [sp, #24]
POP {r6, r4, r5, PC}
ENDPUn Application Binary Interface è un interfaccia che si pone tra due program binary modules (spesso una libreria e un programma eseguito da un utente).
Un aspetto comune di un ABI sono le calling convention, che determinano come i dati sono trasmessi in input oppure letti in output dalle computational routines.
I primi 4 registri r0-r3 (a1-a4) sono utilizzati come argomenti per
le subroutine e il valore di ritorno di da una funzione. Una subroutine
deve sempre preservare il contenuto dei registri r4-r8,
r10, r11 e SP.
Lo standard base proeede du passare gli arcomenti nei core registers (r0-r3) e nello stack. Per le subroutine che richiedono un piccolo numero di parametri, solo i registri sono utilizzati, riducendo in modo significativo l’overhead delle chiamate.
Come già detto lo stack è full-descending, con il current
extent dello stack mantenuto nel registroSP
(R13).
E’ possibile creare delle variabili locali nello stack nello stesso modo in cui salviamo i dati, semplicemnte sottraendo il numero di byte richiesti da ciascuna variabile dallo stack pointer.
I termini di eccezioni e interruzioni sono spesso confusi:
Nelle architetture arm le istruzioni ASM che rilasciano interruzioni software sono le SVC.
Non abbiamo in ARM istruzioni per la gestione di numeri reali.
La interrupt vector Table (IVT) è una tabella che permette di determinare quali procedure gestiscono ciascuna eccezione.
Una Undefined Instruction (UDF) è una istruzione non definita, che potrebbe essere voluto.
SysTick è una eccezione che viene rilasciata dal timer di sistema quando viene raggiunto lo zero. In un sistema operativo, il processore può utilizzare queste eccezioni come system tick.
Attenzione: E’ necessario sapere bene per l’esame come funziona SVC.
Le eccezioni possono trovarsi in 3 differenti stati:
Quando una eccezione viene eseguita, il processore salva le informazioni nello stack corrente. Qeusta operazione è denominata stacking e la struttura di 8 parole è chiamata stack frame.
Gli handler di default sono dichiarati come weak symbols per consentire all’application writer di installare i propri handler semplicemente implementando una funzione con il il nome corretto. Se avviene una interruzione di cui non è stato definito un handler dall’application writer allora viene eseguito quello di default. Il default interrupt handler sono tipicamente implementati come loop infiniti. Se una funzione termina con un handler di default, è prima necessario determinare quale interrupt è in esecuzione.
Le supervisor calls sono normalmente utilizzate per fare richieste di
poerazioni privilegiato o accedere a risorse di sistema da un sistema
operativo. Come gli ARM cores precedenti è presente una istruzione
SVC (Formalmente SWI) che genera una
supervisor call.
La chiamata viene fatta nel seguente modo:
{label} SVC immediateL’istruzione SVC ha un numero al suo interno denominato SVC number. Questo viene utilizzato per indicare chi sta chiamando la richiesta ed è un numero intero su 8 bit da 0 a 255.
Sui core ARM precedenti dovevi estrarre il numero SVC dall’istruzione utilizzando l’indirizzo di ritorno nel collegamento register e gli altri argomenti SVC erano già disponibili da R0 a R3.
Sul Cortex-M3, il core salva i registri degli argomenti nello stack sull’iniziale voce di eccezione. Qualsiasi valore restituito deve essere restituito al chiamante mediante modifica i valori del registro impilati. Per fare ciò, deve essere presente un breve pezzo di codice assembly implementato come inizio del gestore SVC. Per identificare in quale stack sono stati salvati i registri per estrarre il numero SVC dall’istruzione.
Il processore salva un EXC_RETURN value nel
LR una volta che l’eccezione comincia. Questo meccanismo di
eccezione si basa sul valore determinato quando il processore ha
completato l’exception handler. I bit [31:4] di un sono sempre
0xFFFFFF.
Quando il processore carica nel PC un valore che fa il match con il pattern di una EXC_RETURN riconosce che l’operazione non è un normale branch e, invece, che l’eccezione è stata completata. Per questo motivo, se se comincia ritorna una sequenza.
I bit [3:0] Del ECX_RETURN indicano il modo in cui il processore deve ritornare al chiamante.
L’istruzione che segue consente di abilitare l’aggiornamento di registri per utilizzo spaciale quando si ha un livello privilegiato:
MSR {cond} spec_reg, RnI valori sono i seguenti:
cond: è un condition code opzionaleRn: specifica il registro sorgentespec_reg: è uno dei seguenti registri PSR,
IPSR, EPSR, IEPSR,
IAPSR, EAPSR, PSR,
MSP, PSP, PRIMASK,
BASEPRI, BASEPRI_MAX, FAULTMASK,
CONTROLEsistono due modalità operative:
Esistono anche due livelli di accesso:
L’handler mode è sempre privilegiato.
Il control register utilizza i seguenti bit:
0 in handler mode allora MSP è selezionato (non
sono possibili alternate stack per l’handelr mode).0 in thread mode allora il default stack
pointer MSB verrà utilizzato1 in thread mode allora l’alternate stack
pointer PSP verrà utilizzato0 allora il processore è in thread mode in uno stato
privilegiato, se vale 1 allora è in handler mode e in stato
utenteA tempo di reset dopo le inizializzazioni è possibile impostare il processore in user mode.
MOV R0, #3
MSR CONTROL, R0Questa istruzione porta il sistema in uno stato non privilegiato e thread mode, utilizzando il Process Stack Pointer (PSP). A causa dell’ingresso in una procedura di handling il sistema si sta muovendo a uno stato privilegiato e in handler mode, utilizzando il master stack pointer (MSP).
Un esempio:
STACK segment
Stack_Size EQU 0x00000200
AREA STACK, NOINIT, READWRITE, ALIGN=3
SPACE Stack_Size/2
Stack_Process SPACE Stack_Size/2
__initial_spCALLER
MOV R0, #3
MSR CONTROL, R0
LDR SP, =Stack_Process
SVC 0x10HANDLER
STMFD SP!, {R0-R12, LR}
MRS R1, PSP
LDR R0, [R1, #24]
LDR R0, [R0,#-4]
BIC R0, #0xFF000000
LSR R0, #16
LDMFD SP!, {R0-R12, LR}
BX LRPer gestire le periferiche sono possibili due approcci:
Le categorie di eventi di sistema possono essere:
Il polling è un approccio in cui il processore controlla periodicamente lo stato di un dispositivo. Il polling è un approccio semplice ma inefficiente. Il polling è un approccio in cui il processore controlla periodicamente lo stato di un dispositivo. Il polling è un approccio semplice ma inefficiente.
Per determinare se è disponibile o meno si controlla lo status register (best practice) oppure verificando i data registers.
Spesso viene implementato direttamente in software mediante l’utilizzo di un ciclo che esegue una sequenza di check in modo più o meno frequente.
Le caratteristiche principali sono:
Le periferiche possono comunicare direttamente con la CPU mediante le interruzioni, implementate in hardware. In questo modo è possibile entrare in uno stato di sospensione (idle) e il sistema si risveglia quando un evento di sistema si verifica.
Quando viene ricevuta una richiesta, la cpu ha bisogno di riconoscere la sorgente in modo da eseguire il corretto handler.
L’architettura attualmente presa in esame prevede l’implementazione di una gestione vettorizzata delle interruzioni meditante l’utilizzo della IVT (Interrupt Vector Table).
La CPU collabora con i dispositivi esterni mediante l’interrupt controller.
Per configurare la modalità interrupt è necessario a tempo di boot inizializzare le strutture dati come conters, pointers ed eventualmente specificare un flag che consente di abilitare le interrupt (semafori). Inoltre, è necessario configurare l’interrupt controller abilitando le sorgenti e impostando la priorità di ogni sorgente. Questo dovrà essere fatto in ogni routine di servizion per le interruzioni.
Anche a runtime è necessario un acknowledge pulendo i flag che indicano le interruzioni attive, che può essere effettuato in parti differenti della routine.
E’ necessario mantenere il contenuto di R4-R8, R10-R11 (ABI APAPCS) e comunnicando attraverso variabili globali condivise.
Un interrupt controller è un dispositivo che viene utilizzato per combinare diverse sorgenti di interruzioni in una o più linee della cpu, consentendo di gestire più livelli di priorità che possono essere assegnati agli interrupt outputs.
Consente la gestione dei segnali di interruzione ricevuti da più dispositivi combinandoli in un solo interrupt output.
L’interrupt vettoriale annidato Il controller (NVIC) è parte integrante parte del Cortex-M3.
NVIC_SetPriorityPer impostare la priorità abbiamo a disposizione la funzione
NVIC_SetPriority().
void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority);Un valore minore di priorità significa una priorità superiore.
Un sistema potrebbe richiedere di aspettare per un periodo di tempo o eseguire operazioni a periodi regolari. Queste funzionalità potrebbero richiedere il supporto di periferiche denominate timer.
Il timer ha dunque lo scopo di consentire al programmatore di sincronizzare, basandosi sul conteggio, il sistema.
Solitamente quando un procedura di conteggio raggiunge la fine, il sistema regisce in qualche modo:
I timer sono dotati di un clock signal dedicato, attraverso il quale il timer incrementa il suo contatore. I timer hanno dei registri che possono essere programmati con un numero di clock cycle da contare.
Un timer potrebbe seguire differenti modalità di funzionamento:
\[ \text{time}[s] = \text{count} * \text{Clock\_Period}[s] \] \[ \text{count} = \text{time}[s] / \text{Clock\_Period}[s] \] \[ \text{count} = \text{time}[s] * \text{frequency}[1/s] \]
Se il tempo di attesa è troppo alto, potrebbe non rientrare all’interno del timer del registro. Per risolvere tale problema vengono, a seconda dei casi, adoperate soluzioni sia hardware che software:
Normalmente i SoC implementano più timer:
Lo stanrd timer/counter è realizzato in modo da contare i cicli del pheriperical clock (PCLK) o di un clock esterno. Inoltre, può generare facoltativamente interruzioni o eseguire altre azioni per valori specifici del timer in base a 4 match registers.
Prevede 4 capture inputs per trap the timer value quando avviene una transizione del segale, generando opzionalmente una interruzione.
timer 0 e 1 sono di default.
Sono presenti 4 registri a 32 bit che permettono:
Una transizione può essere registrata da un pin configurato per caricare un dei capture registers. Con il valore del timer counter e opzionalmente genera un interrupt.
Quando un match register (MR3:0) è uguale a un timer counter (TC) questo output può essere toggled, andare basso, andare alto o non fare niente.
Per accendere il timer 2 è necessario:
system_LPC17xx.c contenuto nella
cartella lib_SoC_boardconfiguration wizardPCTIM2 e metterla a 1Per abilitare il timer 2 è necessario utilizzare la funzione
enable_timer(x);, con x numero intero del
timer da abilitare.
Il PCON è un registro di controllo che consente di entrare nelle modalità di funzionamento energetico.
Il PCONP è il registro relativo all’alimentazione periferica, utile nel caso si voglia utilizzare il timer 2 e 3.
L’ingresso in modalità di consumo energetico ridotto inizia sempre con l’esecuzione di una WFI (wait for interrupt) o WFE (wait for Exception). Il cortex M3 supporta internamente due modalità di risparmio energetico: sleep e deep sleep. Invece, il power-down e il deep power-down sono selezionati dai bit del registro PCON.
Il registro PCON si occupa anche di gestire alcune modalità di risparmio energetico e altri controlli relativi all’alimentazione. Allo stesso modo, sono presenti dei flag che indicano quando si entra in una situazioone di risparmio energetico.
Il bt PM1 e PM0 di PCON permettono di seleziona una modalità di risparmio energetico. Questi sono encodati in modo da garantire una compatibilità con i dispositivi precedenti che non suppportavano sleep e power-down modes.
Quando la sleep mode è attivata, il clock del core viene fermato e il SMFLAG bit in PCON viene settato. L’esecuzione delle istruzioni viene fermata fino a quando no avviene un reset o una interruzione (il wake up occore quando qualsiasi interruzione occorre).
Le funzioni periferiche continuano a funzionare e potrebbero generare interruzioni che potrebbero risvegliare l’esecuzione. LO sleep mode elminia il dynamic power utilizzato dallo stesso processore, la memoria e il relativi controlli oltre ai bus interni.
Quando il chip entra in deep sleep mode, il clock del core viene fermato interrompendo l’osccilatore principale e il DSFLAG bit in PCON viene settato. L’IRC continua a eseguie e può essere configurato per guidare il watchdog timer, permettendo il watchdog di svegliare la cpu.
L’oscillatore a 32 kHz RTC oscillator non è fermato e il RTC potrebbe essere utilizzato come sorgente di risveglio mediante interruzione.
I PLL sono automaticamente spenti e disconnessi (CCLK e USBCLK vengono resettati a zero).
La memoria FLASH è lasciata in standby in modo da consentire un risveglio rapido.
Dal momento che tutte le operazioni dinamiche sui chip sono sospese, la deep sleep mode riduce il consume energetico dei chip a un valore molto basso. Lo stato del processore e i registri, i registri periferici e la SRAM interna sono preservati e i livelli logici dei pin rimangono statici.
Il deep sleep mode può essere terminato da un reset o da una specifica interruzione in grado di funzionare senza l’utiizzo di clock (Wake-up from Deep Sleep mode can be brought about by NMI or External Interrupts EINT0 through EINT3).
In power down mode avvengono le stesse cose del deep sleep mode ma con la differenza che anche la memoria flash viene posta spenta. Questo consente di risparmiare maggiore energia, ma richiede un tempo di attesa maggiore per il rispeglio della memoria prima dell’esecuzione del codice o dell’acceso ai dati. L’ingresso in questo stato causa il set del bit PDFLAG in PCON.
In deep power-down mode l’alimentazione viene completamente spenta per il cheap ad esclusione del real-time clock, il reset pin, il WIC, il registro di backup RTC. L’ingresso causa il set del bit di DPDFLAG in PCON.
Il power control periferico è gestito dal registro PCONP. Questo consente di spegnere singolarmente le periferiche che non sono necessarie per il funzionamento dell’applicazione, incrementando il risparmio energetico.
Questo risultato viene ottenuto spegnendo la sorgente del clock per determinate periferiche. Alcune funzioni periferiche non possono essere spenti (watchdog timer, il pin connect block, system control block)
Quando un pulsante viene premuto oppure viene invertito un toggle switch, due parti metalliche si uniscono e apparentemente il contatto è immediato. Questo non è completamente corretto in quando all’interno degli switch esistono parti che si muovono.
Quando un pulsante viene premuto, prima avviene il contatto con l’altra parte metallica in un piccolo intervallo di microsecondi. Successivamente esegue un ulteriore contatto leggermente più più lungo e così via, solo alla fine gli switch sono completamente chiusi. Lo switch sta dunque rimbalzando tra “in contatto” e “non in contatto”.
Solitamente il SoC lavora più velocemente del bouncing e per tale motivo l’hardware pensa che il pulsante venga premuto più volte.
UN modo comune per risolvere lo switch bouncing è di rileggere il valore del pin dopo 50ms dal primo bounce. Per il pulsanti si preferisce utilizzare le interruzione (più efficiente energicamente in quanto si può entrare in power down mode), se non è disponibile viene utilizzato il polling (un timer può essere utilizzata pr svegliare il sistema a tempi regolari).
L’implementazione blocking delay non è consigliata, in particolare utilizzare for/while/do-while con blocchi vuoti è considerato profondamente errato.
Se il pin di interrupt mode è settato, questo potrebbe non essere direttamente leggibile. Per tale motivo, per poter leggere il valore del pulsante è necessario disabilitare lei interruzioni e accettare di leggere l’input value.
Quello che succede:
Quello che vogliamo ottenere:
Quanto detto può essere schematizzato nella seguente macchina a stati:
Il RIT è un timer che consente di generare interruzioni a specifici intervalli, senza utilizzare lo standard timer.
// RIT/IRQ_RIT.c
uint32_t init_RIT ( uint32_t RITInterval ){
LPC_SC->PCLKSEL1 &= ~(3<<26);
LPC_SC->PCLKSEL1 |= (1<<26); //RIT Clock = CCLK
...
lib_RIT.c
RIT_cnt = 50ms * 100MHz
RIT_cnt = 5.000.000 = 0x4C4B40
...
}Nota bene: il RIT di default è spento.
Experiment switch bouncing with your board and try to mitigate Key bouncing: they must use the external interrupt functionalities Advanced -> Joystick: implement a «timer controlled polling strategy» also able to mitigate debouncing Quite Advanced -> can you manage the pressur of many buttons or the contemporary use of buttons and Joystick? Super-Advanced -> implement button and joystick debouncing by using the RIT only.
Esistono vari tipi di display, in particolare quella che andremo a vedere è un display dotato di touchscreen. Le due componenti sono in realtà indipendenti e vengono gestite da due interfacce differenti.
Le tipologie sono:
Allo stesso modo anche le tecnologie relative al touchscreen sono diverse:
Nota: Lo schermo che viene utilizzato è capacitivo e di tipo TFT LCD.
Attenzione: Non bisogna oltrepassare i 32kHz di frequenza di clock per il display.
Il display e il touch non sono perfettamente allineati e per tale motivo alla prima accensione deve essere calibrato.
Quando il dito viene tracciato dal display questo appare come un cerchio, ma in realtà il touch panel potrebbe salvare le coordinate come un ellisse. Tale problema può essere causato da alcune trsformazioni come: traslazione, rotazione e scalatura.